package org.oep.jmingle;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import javax.microedition.rms.RecordStoreException;


public class MingleTracker {
	private final String defaultName = "???";
	private final String recordName = "MingleDB";
	private final Hashtable mingledDevices;
	private Vector lastScan;
	private Vector thisScan;
	
	
	// Recaching system
	boolean cached = false;
	long lastCacheTime = -1;			// Helper for timed recaching
	
	// Mingle analytics
	private int mingleDays;				// Number of days we mingled
	private int missDays;				// Number of days not mingled
	private int todaysMingles;
	private int[] slots;	// A cached representation of the day's mingles
	
	public MingleTracker(JMingle parent) {
		thisScan = new Vector();
		lastScan = new Vector();
		mingledDevices = new Hashtable();
	}
	
	
	/*
	 * For external use
	 */
	//--------------------------------------------------------------//
	
	public int getCacheSize() {
		if(mingledDevices != null)
			return mingledDevices.size();
		else
			return -1;
	}
	
	public MingleNode deviceAt(int a) {
		//TODO: This is slow! Find a better way to do it
		if(a < 0 || a >= mingledDevices.size())
			throw new IllegalArgumentException("Index out of range");
		
		Enumeration e = mingledDevices.elements();
		
		int i = 0;
		while(e.hasMoreElements()) {
			if(a == i) return (MingleNode) e.nextElement();
			else e.nextElement();
			i++;
		}
		
		throw new IllegalArgumentException("Index out of range");
	}
	
	public int getRecordSize() {
		try {
			return RecordStoreTools.countRecords(recordName);
		}
		catch(RecordStoreException e) {
			return -1;
		}
	}
	
	public Enumeration getDevices() {
		return mingledDevices.elements();
	}
	
	public void exportToRecordStore() {
		try {
			RecordStoreTools.export(mingledDevices, recordName);
		}
		catch(Exception e) {
			
		}
	}
	
	public void doLoad() {
		if(mingledDevices == null) {
			System.err.println("NULL OMG");
		}
		try {
			RecordStoreTools.load(recordName, mingledDevices);
		} catch (RecordStoreException e) {
			mingledDevices.clear();
		}
	}
	
	public void doMingle(Minglet a) {
		doMingle(a.getAddress(), a.getOwner());
	}
	
	public void doMingle(String address, String name) {
		// Fix the default name
		if(name == null)
			name = defaultName;
		if(name.length() == 0)
			name = defaultName;
		
		MingleNode node = null;
		
		// Here, this device is not new to us
		if(mingledDevices.containsKey(address)){
			boolean updated = false;
			node = (MingleNode) mingledDevices.get(address);
			node.markSeen();
			
			// In this case, we've seen it before, but it's name has changed
			if( shouldDoUpdate(node.getName(), name) ) {
				node.setName(name);
				try {
					if(RecordStoreTools.update(node, recordName)) {
						updated = true;
					}
				}
				catch (RecordStoreException e) { }
			}
			
			node.bump();
			
			// This is to update the time if it hasn't been already
			try {
				if(!updated)
					RecordStoreTools.update(node, recordName);
			}
			catch(RecordStoreException e) { }
				
			
		}
		
		// It's brand-spanking new
		else {
			node = new MingleNode(name, address);
			node.bump();
			
			mingledDevices.put(address, node);
			
			// Add it to the Record Store
			try {
				RecordStoreTools.add(node, recordName);
				// parent.tell("Saved: " + name);
			}
			catch(RecordStoreException e) {
				// parent.tell(e.toString());
			}
		}
		cached = false;
	}
	
	public void eraseAll() {
		mingledDevices.clear();
		thisScan.removeAllElements();
		lastScan.removeAllElements();
		// parent.tell("Deleted mingles in memory");
		try {
			RecordStoreTools.delete(recordName);
		//	parent.tell("Deleted saved mingles");
		}
		catch(RecordStoreException e) {
		//	parent.tell(e.getMessage());
		}
		cached = false;
	}
	
	public boolean know(Minglet a) {
		return know(a.getAddress());
	}
	
	public boolean know(String address) {
		return mingledDevices.containsKey( address );
	}
	
	public String getLastSeenTime(Minglet a) throws Exception {
		return getLastSeenTime(a.getAddress());
	}
	
	public String getLastSeenTime(String address) throws Exception {
		MingleNode n = (MingleNode) mingledDevices.get(address);
		
		if(n == null) throw new Exception("No such key");
		
		return Utils.friendlyTimeDiffNow( n.getLastSeen() );
	}
	
	/**
	 * Returns a (human-unfriendly) timestamp
	 * @param address
	 * @return timestamp
	 * @throws Exception
	 */
	public long getRawLastSeenTime(String address) throws Exception {
		MingleNode n = (MingleNode) mingledDevices.get(address);
		
		if(n == null) throw new Exception("No such key");
		System.out.println("Raw time of: " + n.getLastSeen());
		return n.getLastSeen();
	}
	
	/**
	 * Returns a useful node of data for a given device address
	 * @param hash
	 * @return data with fields for the node
	 */
	public MingleNode getNode(String hash) {
		return (MingleNode) mingledDevices.get(hash);
	}
	
	/**
	 * This function should serve as the common grounds for doing all
	 * of the mingle analytics. A little flag will govern whether or not
	 * we'd like to recache.
	 */
	private void recache() {
		// If it has been over five minutes, go ahead and recache
		long now = System.currentTimeMillis();
		if(now - lastCacheTime > 5 * 60 * 1000) cached = false;
		
		// Stop here if there's no reason to recache
		if(cached) return;
		
		// Basic algorithm: break up the sampling period into 24-hr blocks and make this an
		// array. A mingle day is a day that we have at least one mingle. Otherwise it is
		// a miss day.
		try {
			slots = makeSlots(now);
		}
		catch(Exception e) {
			System.err.println(e);
			lastCacheTime = now;
			mingleDays = 0;
			missDays = 0;
			todaysMingles = 0;
			cached = true;
			return;
		}
		
		if(slots.length == 0) {
			System.err.println("Slots length: 0");
			cached = true;
			return;
		}
		
		System.out.println("*** Recaching analytics ***");
		
		Enumeration e = mingledDevices.elements();
		todaysMingles = 0;
		while(e.hasMoreElements()) {
			MingleNode n = (MingleNode) e.nextElement();
			int index = slots.length - 1 - (int)((now - n.getLastSeen()) / (1000 * 60 * 60 * 24));
			if(index < 0 || index >= slots.length)
				System.err.println("Bad index detected! " + index);
			else {
				System.out.println("Mingled index: " + index);
				slots[index]++;
			}
			
			// If the timestamp is less than 24 hours ago, it happened "today"
			if(now - n.getLastSeen() < 24 * 60 * 60 * 1000) todaysMingles++;
		}
		
		mingleDays = 0;
		for(int i = 0; i < slots.length; i++) {
			if(slots[i] > 0) {
				mingleDays++;
			}
		}
		
		missDays = slots.length - mingleDays;
		cached = true;
		lastCacheTime = now;
		
		System.out.println("Mingle days: " + mingleDays);
		System.out.println("Miss days: " + missDays);
		System.out.println("Today's mingles: " + todaysMingles);
	}
	
	/**
	 * Returns the current energy of the player
	 * @return
	 */
	public int getEnergy() {
		recache();
		return Math.max(0, getHappiness() - todaysMingles);
	}
	
	/**
	 * Returns the current happiness of the player
	 * @return happiness
	 */
	public int getHappiness() {
		recache();
		return Math.max(1, happinessFormula(mingleDays, missDays));
	}
	
	/**
	 * Returns the expected happiness after mingling with a provided address. This function
	 * is not cached so please use sparingly.
	 * @param address
	 * @return expected happiness
	 */
	public int getExpectedHappiness(String address) {
		recache();
		long time;
		int index = 0, resultantMingleDays = 0;
		try { 
			time = getRawLastSeenTime(address);
		}
		catch(Exception e) { time = -1; };
		
		// Basic algorithm: Find the slot where the proposed mingle is located, decrement,
		// increment today's slot and compute the happiness. Then return everything
		// to normal again.
		
		// If time is less than 0, this is a new mingle
		if(time > 0) {
			index = slots.length - 1 - (int)((lastCacheTime - time) / (1000 * 60 * 60 * 24));
			slots[index]--;
		}
		
		// Increment today's mingle
		slots[slots.length - 1]++;
		
		//Calculate the happiness
		for(int i = 0; i < slots.length; i++) {
			if(slots[i] > 0) resultantMingleDays++;
		}
		
		// Fix things back the way they were
		slots[slots.length - 1]--;
		if(time > 0) { slots[index]++; }
		
		return MingleTracker.happinessFormula(resultantMingleDays, slots.length - resultantMingleDays);
	}
	
	public static int happinessFormula(int mingleDays, int missDays) {
		if(mingleDays > 1)
			return Math.max(1, mingleDays - ((missDays - 1) / (mingleDays - 1)));
		else return 1;
	}
	
	private int[] makeSlots(long now) throws Exception {
		long first = getEarliestMingle();
		int days = Math.max(1, (int) Math.ceil(((now - first) / (1000 * 60 * 60 * 24))) );
		int[] slots = new int[days];
		for(int i = 0; i < slots.length; i++) slots[i] = 0;
		return slots;
	}
	
	private long getEarliestMingle() throws Exception {
		System.out.println("Size: " + mingledDevices.size());
		Enumeration e = mingledDevices.elements();
		long min;
		if(e.hasMoreElements()) {
			min = ((MingleNode)e.nextElement()).getLastSeen();
		}
		else
			throw new Exception("No mingles");
		
		while(e.hasMoreElements()) {
			MingleNode n = (MingleNode) e.nextElement();
			min = Math.min(min, n.getLastSeen());
		}
		return min;
	}

	private boolean shouldDoUpdate(String o, String n) {
		// Never update to the default name
		if(n.compareTo(defaultName) == 0) return false;
		
		// Never update if names are the same
		if(o.compareTo(n) == 0) return false;
		
		return true;
	}
}
